package club.bigtian.method.handler;

import club.bigtian.method.anno.BigFunction;
import club.bigtian.method.anno.BigMethod;
import club.bigtian.method.core.MethodInfo;
import club.bigtian.method.util.*;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;

import com.alibaba.fastjson2.JSON;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Collectors;

@SupportedAnnotationTypes({"club.bigtian.method.anno.BigFunction"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BigFunctionAbstractProcessor extends AbstractProcessor {
    protected JavacTrees trees;
    protected TreeMaker treeMaker;
    protected Names names;
    JavacElements elementUtils;
    public static Messager messager;

    public BigFunctionAbstractProcessor() {
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        ProcessingEnvironment processingEnv1 = getJavacProcessingEnvironment(processingEnv);
        this.trees = JavacTrees.instance(processingEnv1);
        Context context = ((JavacProcessingEnvironment) processingEnv1).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
        this.elementUtils = (JavacElements) processingEnv.getElementUtils();
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BigFunction.class);
        for (Element element : elements) {
            elementUtils.getTree(element);
        }
        elements.stream().map((element) -> {
            return elementUtils.getTree(element);
            // return this.trees.getTree(element);
        }).forEach((jcTree) -> {

            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<MethodInfo> methodList = new ArrayList();
                    JCTree.JCAnnotation annotation = ClassUtil.getAnnoByType(jcClassDecl, BigFunction.class);
                    MethodInfo info = AnnoUtil.getAnnotationValue(annotation);
                    List<JCTree.JCMethodDecl> list = MethodUtils.getMethodByAnno(jcClassDecl, BigMethod.class);

                    if (CollUtil.isNotEmpty(list)) {
                        list.forEach((jcMethodDecl) -> {
                            MethodInfo methodInfo = AnnoUtil.getAnnotationValue(AnnoUtil.getAnno(jcMethodDecl, BigMethod.class));
                            methodInfo.setMethodName(jcMethodDecl.getName().toString());
                            methodList.add(methodInfo);
                        });
                    }
                    String[] filedType = FieldUtil.getFiledType(jcClassDecl, info.getTarget()[0]);
                    List<JCTree.JCMethodDecl> methodAll = MethodUtils.getMethodAll(jcClassDecl, filedType);
                    String[] target = info.getTarget();
                    List<String> methodNameList = methodList.stream()
                            .map(MethodInfo::getMethodName)
                            .collect(Collectors.toList());
                    for (JCTree.JCMethodDecl jcMethodDecl : methodAll) {
                        if (methodNameList.contains(jcMethodDecl.name.toString())) {
                            continue;
                        }
                        info = new MethodInfo();
                        info.setTarget(target);
                        info.setName(jcMethodDecl.name.toString());
                        info.setMethodName(jcMethodDecl.name.toString());
                        methodList.add(info);
                    }
                    jcClassDecl.defs = jcClassDecl.defs.prepend(BigFunctionAbstractProcessor.this.makeMethodDecl(methodList));
                    super.visitClassDef(jcClassDecl);
                }
            });
        });
        return false;
    }

    public static ProcessingEnvironment getJavacProcessingEnvironment(ProcessingEnvironment procEnv) {
        return tryRecursivelyObtainJavacProcessingEnvironment(procEnv);
    }

    private static ProcessingEnvironment tryRecursivelyObtainJavacProcessingEnvironment(ProcessingEnvironment procEnv) {
        if (procEnv.getClass().getName().equals("com.sun.tools.javac.processing.JavacProcessingEnvironment")) {
            return procEnv;
        }

        for (Class<?> procEnvClass = procEnv.getClass(); procEnvClass != null; procEnvClass = procEnvClass.getSuperclass()) {
            try {
                Object delegate = tryGetDelegateField(procEnvClass, procEnv);
                if (delegate == null) delegate = tryGetProcessingEnvField(procEnvClass, procEnv);
                if (delegate == null) delegate = tryGetProxyDelegateToField(procEnvClass, procEnv);

                if (delegate != null)
                    return tryRecursivelyObtainJavacProcessingEnvironment((ProcessingEnvironment) delegate);
            } catch (final Exception e) {
                // no valid delegate, try superclass
            }
        }

        return null;
    }

    /**
     * Gradle incremental processing
     */
    private static Object tryGetDelegateField(Class<?> delegateClass, Object instance) {
        try {
            return Permit.getField(delegateClass, "delegate").get(instance);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Kotlin incremental processing
     */
    private static Object tryGetProcessingEnvField(Class<?> delegateClass, Object instance) {
        try {
            return Permit.getField(delegateClass, "processingEnv").get(instance);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * IntelliJ IDEA >= 2020.3
     */
    private static Object tryGetProxyDelegateToField(Class<?> delegateClass, Object instance) {
        try {
            InvocationHandler handler = Proxy.getInvocationHandler(instance);
            return Permit.getField(handler.getClass(), "val$delegateTo").get(handler);
        } catch (Exception e) {
            return null;
        }
    }

    public JCTree.JCMethodDecl makeMethodDecl(List<MethodInfo> list) {
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer();
        for (MethodInfo info : list) {
            String methodName = info.getMethodName().replaceAll("\"", "");
            String[] target = info.getTarget();
            for (String targetMap : target) {
                JCTree.JCExpression thisTest = treeMaker.Ident(names.fromString("this"));
                JCTree.JCMemberReference thisTestRef = treeMaker.Reference(JCTree.JCMemberReference.ReferenceMode.INVOKE, names.fromString(methodName), thisTest, null);
                statements.append(this.treeMaker.Exec(this.treeMaker.Apply(com.sun.tools.javac.util.List.nil(), this.treeMaker.Select(this.treeMaker.Ident(this.elementUtils.getName(targetMap)), this.elementUtils.getName("put")),
                        com.sun.tools.javac.util.List.of(this.treeMaker.Literal(info.getName()),
                                thisTestRef))));
            }
        }
        JCTree.JCBlock body = this.treeMaker.Block(0L, statements.toList());
        JCTree.JCAnnotation jcAnnotation = this.treeMaker.Annotation(this.memberAccess("javax.annotation.PostConstruct"), com.sun.tools.javac.util.List.nil());
        com.sun.tools.javac.util.List<JCTree.JCAnnotation> jcAnnotationList = com.sun.tools.javac.util.List.nil();
        jcAnnotationList = jcAnnotationList.append(jcAnnotation);
        JCTree.JCMethodDecl jcMethodDecl = this.treeMaker.MethodDef(this.treeMaker.Modifiers(1L, jcAnnotationList), this.getNewMethodName(), this.treeMaker.TypeIdent(TypeTag.VOID), com.sun.tools.javac.util.List.nil(), com.sun.tools.javac.util.List.nil(), com.sun.tools.javac.util.List.nil(), body, null);
        return jcMethodDecl;
    }

    private JCTree.JCExpression memberAccess(String components) {
        String[] componentArray = components.split("\\.");
        JCTree.JCExpression expr = this.treeMaker.Ident(this.names.fromString(componentArray[0]));

        for (int i = 1; i < componentArray.length; ++i) {
            expr = this.treeMaker.Select(expr, this.names.fromString(componentArray[i]));
        }

        return expr;
    }

    private Name getNewMethodName() {
        return this.names.fromString("initMethod");
    }
}
